/*******************************************************************************
 * Copyright (c) 2011, 2016 Eurotech and/or its affiliates
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Eurotech
 *******************************************************************************/
package org.eclipse.kura.linux.net.route;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;

import org.eclipse.kura.core.util.ProcessUtil;
import org.eclipse.kura.core.util.SafeProcess;
import org.eclipse.kura.net.IP4Address;
import org.eclipse.kura.net.IP6Address;
import org.eclipse.kura.net.IPAddress;
import org.eclipse.kura.net.route.RouteConfig;
import org.eclipse.kura.net.route.RouteConfigIP4;
import org.eclipse.kura.net.route.RouteConfigIP6;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

//public class RouteServiceImpl implements RouteService, IConfigurableComponentService {
public class RouteServiceImpl implements RouteService {

    private static Logger s_logger = LoggerFactory.getLogger(RouteServiceImpl.class);

    private static RouteServiceImpl s_routeService = null;

    // public static final String CONFIGURATION_NAME = "org.eclipse.kura.net.linux.route";

    private String m_osRouteConfigDirectory = null;

    private RouteServiceImpl() {
        this.m_osRouteConfigDirectory = "/etc/sysconfig/network-scripts/";
    }

    public static synchronized RouteService getInstance() {
        if (s_routeService == null) {
            s_routeService = new RouteServiceImpl();
        }

        return s_routeService;
    }

    private void addPersistentStaticRoute(IPAddress destination, IPAddress gateway, IPAddress netmask, String iface)
            throws Exception {
        addStaticRoute(destination, gateway, netmask, iface, 0);
        RouteFile routeFile = new RouteFile(iface);
        if (routeFile.addRoute(destination, gateway, netmask, iface)) {
            s_logger.info("Route persistence added");
        } else {
            s_logger.error("Error adding route persistence");
        }
    }

    @Override
    public void addStaticRoute(IPAddress destination, IPAddress gateway, IPAddress netmask, String iface, int metric)
            throws Exception {
        RouteConfig tmpRoute = null;
        StringBuffer command = new StringBuffer();
        command.append("route add -net " + destination.getHostAddress() + " ");
        if (netmask != null) {
            command.append("netmask " + netmask.getHostAddress() + " ");
        }
        if (gateway != null) {
            if (gateway.getHostAddress().compareTo("0.0.0.0") != 0
                    && gateway.getHostAddress().compareTo("127.0.0.1") != 0) {
                command.append("gw " + gateway.getHostAddress() + " ");
            }
        }
        if (iface != null) {
            command.append("dev " + iface + " ");
        }
        if (metric != 0 && metric != -1) {
            command.append("metric " + metric);
        }

        SafeProcess proc = null;
        try {
            s_logger.debug("Executing command:  {}", command.toString());
            proc = ProcessUtil.exec(command.toString());
            proc.waitFor();
            if (proc.exitValue() != 0) {
                s_logger.error("Error adding static Route: " + command.toString());
                throw new Exception("Error adding Static Route");
            }
        } catch (IOException e) {
            s_logger.error("Error executing command:  route -n");
            throw e;
        } finally {
            if (proc != null) {
                ProcessUtil.destroy(proc);
            }
        }

        if (destination instanceof IP4Address) {
            tmpRoute = new RouteConfigIP4((IP4Address) destination, (IP4Address) gateway, (IP4Address) netmask, iface,
                    -1);
        } else if (destination instanceof IP6Address) {
            tmpRoute = new RouteConfigIP6((IP6Address) destination, (IP6Address) gateway, (IP6Address) netmask, iface,
                    -1);
        }
        s_logger.info("Static route added successfully");
        s_logger.debug(tmpRoute.getDescription());
    }

    @Override
    public RouteConfig getDefaultRoute(String iface) {
        RouteConfig[] routes = getRoutes();
        RouteConfig defaultRoute;
        ArrayList<RouteConfig> defaultRoutes = new ArrayList<RouteConfig>();

        // Search through routes and construct a list of all default routes for the specified interface
        for (RouteConfig route : routes) {
            if (route.getInterfaceName().compareTo(iface) == 0
                    && route.getDestination().getHostAddress().compareTo("0.0.0.0") == 0) {
                defaultRoutes.add(route);
            }
        }

        // If no default routes exist, return null
        if (defaultRoutes.size() == 0) {
            s_logger.debug("No default routes exist for inteface: {}", iface);
            return null;
        }

        // Set the default route to the first one in the list
        defaultRoute = defaultRoutes.get(0);

        // Search for the default route with the lowest metric value
        for (int i = 1; i < defaultRoutes.size(); i++) {
            if (defaultRoute.getMetric() > defaultRoutes.get(i).getMetric()) {
                defaultRoute = defaultRoutes.get(i);
            }
        }

        s_logger.info("Default route found for interface: " + iface);
        s_logger.debug("Default route:\n{}", defaultRoute.getDescription());
        return defaultRoute;
    }

    @Override
    public RouteConfig[] getRoutes() {
        String routeEntry = null;
        ArrayList<RouteConfig> routeList = new ArrayList<RouteConfig>();
        RouteConfig[] routes = null;
        RouteConfig tmpRoute = null;
        SafeProcess proc = null;
        BufferedReader br = null;
        try {
            proc = ProcessUtil.exec("route -n");
            proc.waitFor();
            br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
            br.readLine();
            br.readLine();
            while ((routeEntry = br.readLine()) != null) {
                tmpRoute = entryToRoute(routeEntry);
                if (tmpRoute != null) {
                    routeList.add(tmpRoute);
                }
            }
        } catch (Exception e) {
            s_logger.error("Error executing command:  route -n", e);
            return null;
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException ex) {
                    s_logger.error("I/O Exception while closing BufferedReader!");
                }
            }
            if (proc != null) {
                ProcessUtil.destroy(proc);
            }
        }

        routes = new RouteConfig[routeList.size()];
        for (int i = 0; i < routes.length; i++) {
            routes[i] = routeList.get(i);
        }

        return routes;
    }

    private void removePersistentStaticRoute(IPAddress destination, IPAddress gateway, IPAddress netmask, String iface)
            throws Exception {
        removeStaticRoute(destination, gateway, netmask, iface);
        RouteFile routeFile = new RouteFile(iface);
        if (routeFile.removeRoute(destination, gateway, netmask, iface)) {
            s_logger.info("Route persistence removed");
        } else {
            s_logger.error("Error removing route persistence");
        }
    }

    @Override
    public void removeStaticRoute(IPAddress destination, IPAddress gateway, IPAddress netmask, String iface)
            throws Exception {
        RouteConfig tmpRoute = null;
        StringBuffer command = new StringBuffer();
        command.append("route del -net " + destination.getHostAddress() + " ");
        if (netmask != null) {
            command.append("netmask " + netmask.getHostAddress() + " ");
        }
        if (gateway != null) {
            if (gateway.getHostAddress().compareTo("127.0.0.1") != 0) {
                command.append("gw " + gateway.getHostAddress() + " ");
            }
        }
        if (iface != null) {
            command.append("dev " + iface + " ");
        }

        SafeProcess proc = null;
        try {
            s_logger.debug("Executing command: {}", command.toString());
            proc = ProcessUtil.exec(command.toString());
            proc.waitFor();
            if (proc.exitValue() != 0) {
                s_logger.error("Error removing static Route");
                throw new Exception("Error removing Static Route");
            }
        } catch (IOException e) {
            s_logger.error("Error executing command:  route -n");
            throw e;
        } finally {
            if (proc != null) {
                ProcessUtil.destroy(proc);
            }
        }

        if (destination instanceof IP4Address) {
            tmpRoute = new RouteConfigIP4((IP4Address) destination, (IP4Address) gateway, (IP4Address) netmask, iface,
                    -1);
        } else if (destination instanceof IP6Address) {
            tmpRoute = new RouteConfigIP6((IP6Address) destination, (IP6Address) gateway, (IP6Address) netmask, iface,
                    -1);
        }
        s_logger.info("Static route removed successfully");
        s_logger.debug(tmpRoute.getDescription());
    }

    private RouteConfig entryToRoute(String entry) {
        RouteConfig route;
        IPAddress destination;
        IPAddress gateway;
        IPAddress netmask;
        int metric;
        String iface;
        String tmp;

        try {
            route = null;
            StringTokenizer tok = new StringTokenizer(entry, " ");
            tmp = tok.nextToken();
            destination = IPAddress.parseHostAddress(tmp);
            gateway = IPAddress.parseHostAddress(tok.nextToken());
            netmask = IPAddress.parseHostAddress(tok.nextToken());
            tok.nextToken();
            metric = Integer.parseInt(tok.nextToken());
            tok.nextToken();
            tok.nextToken();
            iface = tok.nextToken();
        } catch (Exception e) {
            s_logger.error("Error parsing route table entry:  " + e.getMessage());
            e.printStackTrace();
            return null;
        }

        if (destination instanceof IP4Address) {
            route = new RouteConfigIP4((IP4Address) destination, (IP4Address) gateway, (IP4Address) netmask, iface,
                    metric);
        } else if (destination instanceof IP6Address) {
            route = new RouteConfigIP6((IP6Address) destination, (IP6Address) gateway, (IP6Address) netmask, iface,
                    metric);
        }
        s_logger.trace("Route successfully read from route table entry");
        return route;
    }

    @Override
    public String getDefaultInterface(IPAddress destination) {
        ArrayList<RouteConfig> matches = new ArrayList<RouteConfig>();
        RouteConfig[] routes = getRoutes();
        for (RouteConfig route : routes) {
            if (matchesRoute(destination, route)) {
                matches.add(route);
            }
        }
        if (matches.size() > 0) {
            RouteConfig dRoute = matches.get(0);
            for (RouteConfig route : routes) {
                if (dRoute.getMetric() > route.getMetric()) {
                    dRoute = route;
                }
            }
            s_logger.debug("Found defualt interface " + dRoute.getInterfaceName() + " for destination "
                    + destination.getHostAddress());
            return dRoute.getInterfaceName();
        }
        s_logger.debug("No default interface exists for destination {}", destination.getHostAddress());
        return null;
    }

    private boolean matchesRoute(IPAddress destination, RouteConfig route) {
        byte mask = (byte) 0xFF;
        byte[] dest = destination.getAddress();
        byte[] routeMask = route.getNetmask().getAddress();
        byte[] routeDest = route.getDestination().getAddress();
        byte[] dest_masked = new byte[4];
        for (int i = 0; i < 4; i++) {
            if (routeMask[i] == mask) {
                dest_masked[i] = dest[i];
            } else {
                dest_masked[i] = 0;
            }
        }
        for (int i = 0; i < 4; i++) {
            if (dest_masked[i] != routeDest[i]) {
                return false;
            }
        }
        return true;
    }

    /*
     * Container class for a new route from a configuration
     */
    private class ConfigRoute {

        public int index;
        public IPAddress destination = null;
        public IPAddress netmask = null;
        public IPAddress gateway = null;
        public String iface = null;

        public ConfigRoute(int index) {
            this.index = index;
        }

        public boolean isComplete() {
            return this.destination != null && this.netmask != null && this.gateway != null && this.iface != null;
        }

        public String getDescription() {
            return "dest: " + this.destination.getHostAddress() + " nm: " + this.netmask.getHostAddress() + " gw: "
                    + this.gateway.getHostAddress() + " iface: " + this.iface;
        }
    }

    // public Object receiveConfig(Object config) throws KuraConfigurationException {
    // Properties props = (Properties)config;
    // Enumeration keys = props.keys();
    // ArrayList newRoutes = new ArrayList();
    // String key = null;
    // String param = null;
    // int index = 0;
    // ConfigRoute newRoute = null;
    // s_logger.debug("New Configuration received with " + props.size() + " elements");
    //
    // // Validate the configuration parameters and create ConfigRoute objects to hold the new routes
    // while(keys.hasMoreElements()) {
    // key = (String)keys.nextElement();
    // try {
    // index = getKeyIndex(key);
    // } catch (NumberFormatException e) {
    // s_logger.error("Error parsing configuration parameter " + key);
    // throw new KuraConfigurationException("Error parsing configuration parameter: " + key + ", " + e.getMessage());
    // }
    // param = getKeyParam(key);
    //
    // for(int i=0; i<newRoutes.size(); i++) {
    // ConfigRoute tmpRoute = (ConfigRoute)newRoutes.get(i);
    // if(tmpRoute.index == index) {
    // s_logger.debug("New parameter found for route " + index);
    // newRoute = tmpRoute;
    // }
    // }
    //
    // if(newRoute == null) {
    // s_logger.debug("New parameter found for NEW route " + index);
    // newRoute = new ConfigRoute(index);
    // newRoutes.add(newRoute);
    // }
    //
    // try {
    // if(param.compareTo("destination")==0) {
    // s_logger.debug("New Configuration - Route - destination: " + (String)props.get(key));
    // newRoute.destination = InetAddress.getByName((String)props.get(key));
    // } else if(param.compareTo("netmask")==0) {
    // s_logger.debug("New Configuration - Route - netmask: " + (String)props.get(key));
    // newRoute.netmask = InetAddress.getByName((String)props.get(key));
    // } else if(param.compareTo("gateway")==0) {
    // s_logger.debug("New Configuration - Route - gateway: " + (String)props.get(key));
    // newRoute.gateway = InetAddress.getByName((String)props.get(key));
    // } else if(param.compareTo("iface")==0) {
    // s_logger.debug("New Configuration - Route - iface: " + (String)props.get(key));
    // newRoute.iface = (String)props.get(key);
    // } else {
    // s_logger.error("Error parsing configuration, unknown parameter: " + key);
    // throw new KuraConfigurationException("Unknown parameter: " + key);
    // }
    // } catch (UnknownHostException e) {
    // e.printStackTrace();
    // s_logger.error("Error parsing configuration, unknown host: " + (String)props.get(key));
    // throw new KuraConfigurationException("Unknown host: " + (String)props.get(key));
    //
    // }
    // newRoute = null;
    // }
    //
    // // Make sure all new route configurations are complete
    // for(int i=0; i<newRoutes.size(); i++) {
    // ConfigRoute cr = (ConfigRoute)newRoutes.get(i);
    // if(!cr.isComplete()) {
    // s_logger.error("Error parsing configuration, missing parameters for new route " + cr.index);
    // throw new KuraConfigurationException("Missing parameters for new route " + cr.index);
    // } else {
    // s_logger.debug("New complete route configuration: " + cr.getDescription());
    // }
    // }
    //
    // // Clear current OS persistent route files
    // clearPersistenceFiles();
    //
    // // Create new routes
    // RouteFile routeFile;
    // for(int i=0; i<newRoutes.size(); i++) {
    // ConfigRoute cr = (ConfigRoute)newRoutes.get(i);
    // s_logger.debug("editing route file for interface: " + cr.iface);
    // routeFile = new RouteFile(cr.iface);
    // s_logger.debug("adding route: " + cr.getDescription());
    // routeFile.addRoute(cr.destination, cr.gateway, cr.netmask, cr.iface);
    // }
    //
    // return null;
    // }

    private int getKeyIndex(String key) throws NumberFormatException {
        int index;
        try {
            String indexString = key.substring(key.indexOf("_") + 1);
            index = Integer.parseInt(indexString);
        } catch (NumberFormatException e) {
            e.printStackTrace();
            throw e;
        }
        return index;
    }

    private String getKeyParam(String key) {
        return key.substring(0, key.indexOf("_"));
    }

    private void clearPersistenceFiles() {
        File dir = new File(this.m_osRouteConfigDirectory);
        String[] files = dir.list();
        for (String file : files) {
            if (file.startsWith("route-")) {
                File fileToDelete = new File(this.m_osRouteConfigDirectory + file);
                fileToDelete.delete();
            }
        }
    }
}
